Несмотря на огромные вложения в рекламу развлекательного приложения Procrastinate Pro+, последние несколько месяцев компания терпит убытки. Задача — разобраться в причинах и помочь компании выйти в плюс.
Цель исследования изучить:
Исследование пройдет в несколько этапов:
Есть данные о пользователях, привлечённых с 1 мая по 27 октября 2019 года:
visits_info_short.csv хранит лог сервера с информацией о посещениях сайта, orders_info_short.csv — информацию о заказах, costs_info_short.csv — информацию о расходах на рекламу.Структура visits_info_short.csv:
Структура orders_info_short.csv:
Структура costs_info_short.csv:
import pandas as pd
import numpy as np
import os
from datetime import datetime, timedelta
from matplotlib import pyplot as plt
pd.options.display.float_format = '{:.2f}'.format
Прочитаем файлы и сохраним их в переменной df. Предусмотрим чтение файла из локального хранилища и из облака Яндекс. Выведем на экран 10 первых строк.
def read_file(file, number):
'''Функция обзора данных.
file - имя файла в формате 'xxxx.csv'
number - целое число.
Считывает файл из облака Яндекс.Практикума или из домашнего каталога с ноутбуком.
Выводит на экран number-строк датафрейма и общую информацию о количестве строк и типах данных.
'''
if os.path.exists('----' + file): # проверка наличия файла в облаке
df = pd.read_csv('----' + file) # чтение файла с данными и сохранение
else:
try:
df = pd.read_csv(file)
except:
display('Проверьте наличие файла на локальном хранилище!')
display(df.head(number))
print()
display(df.info())
print()
# подсчет уникальных значений по столбцу и вывод их на экран (если количество не превышает 20)
for col in df.columns:
print( col, df[col].nunique())
print('минимальное значение', df[col].min())
print('максимальное значение', df[col].max())
if df[col].nunique() < 20:
print(df[col].sort_values().unique())
print()
print('Количество дубликатов', df.duplicated().sum())
shape = df.shape
return df, shape
visits, visits_shape = read_file('----.csv', 5)
| User Id | Region | Device | Channel | Session Start | Session End | |
|---|---|---|---|---|---|---|
| 0 | 981449118918 | United States | iPhone | organic | 2019-05-01 02:36:01 | 2019-05-01 02:45:01 |
| 1 | 278965908054 | United States | iPhone | organic | 2019-05-01 04:46:31 | 2019-05-01 04:47:35 |
| 2 | 590706206550 | United States | Mac | organic | 2019-05-01 14:09:25 | 2019-05-01 15:32:08 |
| 3 | 326433527971 | United States | Android | TipTop | 2019-05-01 00:29:59 | 2019-05-01 00:54:25 |
| 4 | 349773784594 | United States | Mac | organic | 2019-05-01 03:33:35 | 2019-05-01 03:57:40 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 309901 entries, 0 to 309900 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 User Id 309901 non-null int64 1 Region 309901 non-null object 2 Device 309901 non-null object 3 Channel 309901 non-null object 4 Session Start 309901 non-null object 5 Session End 309901 non-null object dtypes: int64(1), object(5) memory usage: 14.2+ MB
None
User Id 150008 минимальное значение 599326 максимальное значение 999999563947 Region 4 минимальное значение France максимальное значение United States ['France' 'Germany' 'UK' 'United States'] Device 4 минимальное значение Android максимальное значение iPhone ['Android' 'Mac' 'PC' 'iPhone'] Channel 11 минимальное значение AdNonSense максимальное значение organic ['AdNonSense' 'FaceBoom' 'LeapBob' 'MediaTornado' 'OppleCreativeMedia' 'RocketSuperAds' 'TipTop' 'WahooNetBanner' 'YRabbit' 'lambdaMediaAds' 'organic'] Session Start 306813 минимальное значение 2019-05-01 00:00:41 максимальное значение 2019-10-31 23:59:23 Session End 306793 минимальное значение 2019-05-01 00:07:06 максимальное значение 2019-11-01 01:38:46 Количество дубликатов 0
Session Start, Session End в формат datetime.orders, orders_shape = read_file('----.csv', 10)
| User Id | Event Dt | Revenue | |
|---|---|---|---|
| 0 | 188246423999 | 2019-05-01 23:09:52 | 4.99 |
| 1 | 174361394180 | 2019-05-01 12:24:04 | 4.99 |
| 2 | 529610067795 | 2019-05-01 11:34:04 | 4.99 |
| 3 | 319939546352 | 2019-05-01 15:34:40 | 4.99 |
| 4 | 366000285810 | 2019-05-01 13:59:51 | 4.99 |
| 5 | 129100331057 | 2019-05-01 01:56:28 | 4.99 |
| 6 | 626807144131 | 2019-05-01 02:39:21 | 4.99 |
| 7 | 914062168985 | 2019-05-01 02:02:18 | 4.99 |
| 8 | 769669137453 | 2019-05-01 03:39:47 | 4.99 |
| 9 | 434198630691 | 2019-05-01 18:16:50 | 5.99 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 40212 entries, 0 to 40211 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 User Id 40212 non-null int64 1 Event Dt 40212 non-null object 2 Revenue 40212 non-null float64 dtypes: float64(1), int64(1), object(1) memory usage: 942.6+ KB
None
User Id 8881 минимальное значение 599326 максимальное значение 999895427370 Event Dt 40163 минимальное значение 2019-05-01 00:28:11 максимальное значение 2019-10-31 23:56:56 Revenue 5 минимальное значение 4.99 максимальное значение 49.99 [ 4.99 5.99 9.99 19.99 49.99] Количество дубликатов 0
Event Dt в формат datetime.costs, costs_shape = read_file('----.csv', 10)
| dt | Channel | costs | |
|---|---|---|---|
| 0 | 2019-05-01 | FaceBoom | 113.30 |
| 1 | 2019-05-02 | FaceBoom | 78.10 |
| 2 | 2019-05-03 | FaceBoom | 85.80 |
| 3 | 2019-05-04 | FaceBoom | 136.40 |
| 4 | 2019-05-05 | FaceBoom | 122.10 |
| 5 | 2019-05-06 | FaceBoom | 118.80 |
| 6 | 2019-05-07 | FaceBoom | 101.20 |
| 7 | 2019-05-08 | FaceBoom | 100.10 |
| 8 | 2019-05-09 | FaceBoom | 93.50 |
| 9 | 2019-05-10 | FaceBoom | 104.50 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1800 entries, 0 to 1799 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 dt 1800 non-null object 1 Channel 1800 non-null object 2 costs 1800 non-null float64 dtypes: float64(1), object(2) memory usage: 42.3+ KB
None
dt 180 минимальное значение 2019-05-01 максимальное значение 2019-10-27 Channel 10 минимальное значение AdNonSense максимальное значение lambdaMediaAds ['AdNonSense' 'FaceBoom' 'LeapBob' 'MediaTornado' 'OppleCreativeMedia' 'RocketSuperAds' 'TipTop' 'WahooNetBanner' 'YRabbit' 'lambdaMediaAds'] costs 608 минимальное значение 0.8 максимальное значение 630.0 Количество дубликатов 0
dt к типу datetime.Преобразуем колонки с датами к типу datetime. Приведем названия колонок в соответствие со стилем.
def column_style(df):
'''Преобразование названий колонок. Перевод в нижний регистр, замена пробелов на нижнее подчеркивание'''
df.columns = df.columns.str.lower().str.replace(' ', '_')
visits['Session Start'] = pd.to_datetime(visits['Session Start'])
visits['Session End'] = pd.to_datetime(visits['Session End'])
column_style(visits)
visits.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 309901 entries, 0 to 309900 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 309901 non-null int64 1 region 309901 non-null object 2 device 309901 non-null object 3 channel 309901 non-null object 4 session_start 309901 non-null datetime64[ns] 5 session_end 309901 non-null datetime64[ns] dtypes: datetime64[ns](2), int64(1), object(3) memory usage: 14.2+ MB
orders['Event Dt'] = pd.to_datetime(orders['Event Dt'])
column_style(orders)
orders.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 40212 entries, 0 to 40211 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 40212 non-null int64 1 event_dt 40212 non-null datetime64[ns] 2 revenue 40212 non-null float64 dtypes: datetime64[ns](1), float64(1), int64(1) memory usage: 942.6 KB
costs['dt'] = pd.to_datetime(costs['dt'])
column_style(costs)
costs.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1800 entries, 0 to 1799 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 dt 1800 non-null datetime64[ns] 1 channel 1800 non-null object 2 costs 1800 non-null float64 dtypes: datetime64[ns](1), float64(1), object(1) memory usage: 42.3+ KB
# функция для создания пользовательских профилей
def get_profiles(sessions, orders, ad_costs, event_names=[]):
# находим параметры первых посещений
profiles = (
sessions.sort_values(by=['user_id', 'session_start'])
.groupby('user_id')
.agg(
{
'session_start': 'first',
'channel': 'first',
'device': 'first',
'region': 'first',
}
)
.rename(columns={'session_start': 'first_ts'})
.reset_index()
)
# для когортного анализа определяем дату первого посещения
# и первый день месяца, в который это посещение произошло
profiles['dt'] = profiles['first_ts'].astype('datetime64[D]')
profiles['month'] = profiles['first_ts'].astype('datetime64[M]')
# добавляем признак платящих пользователей
profiles['payer'] = profiles['user_id'].isin(orders['user_id'].unique())
# считаем количество уникальных пользователей
# с одинаковыми источником и датой привлечения
new_users = (
profiles.groupby(['dt', 'channel'])
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'unique_users'})
.reset_index()
)
# объединяем траты на рекламу и число привлечённых пользователей
ad_costs = ad_costs.merge(new_users, on=['dt', 'channel'], how='left')
# делим рекламные расходы на число привлечённых пользователей
ad_costs['acquisition_cost'] = ad_costs['costs'] / ad_costs['unique_users']
# добавляем стоимость привлечения в профили
profiles = profiles.merge(
ad_costs[['dt', 'channel', 'acquisition_cost']],
on=['dt', 'channel'],
how='left',
)
# стоимость привлечения органических пользователей равна нулю
profiles['acquisition_cost'] = profiles['acquisition_cost'].fillna(0)
return profiles
# функция для расчёта удержания
def get_retention(
profiles,
sessions,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# добавляем столбец payer в передаваемый dimensions список
dimensions = ['payer'] + dimensions
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# собираем «сырые» данные для расчёта удержания
result_raw = result_raw.merge(
sessions[['user_id', 'session_start']], on='user_id', how='left'
)
result_raw['lifetime'] = (
result_raw['session_start'] - result_raw['first_ts']
).dt.days
# функция для группировки таблицы по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
result = df.pivot_table(
index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
)
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
result = result.div(result['cohort_size'], axis=0)
result = result[['cohort_size'] + list(range(horizon_days))]
result['cohort_size'] = cohort_sizes
return result
# получаем таблицу удержания
result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)
# получаем таблицу динамики удержания
result_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
# возвращаем обе таблицы и сырые данные
return result_raw, result_grouped, result_in_time
# функция для расчёта конверсии
def get_conversion(
profiles,
purchases,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# определяем дату и время первой покупки для каждого пользователя
first_purchases = (
purchases.sort_values(by=['user_id', 'event_dt'])
.groupby('user_id')
.agg({'event_dt': 'first'})
.reset_index()
)
# добавляем данные о покупках в профили
result_raw = result_raw.merge(
first_purchases[['user_id', 'event_dt']], on='user_id', how='left'
)
# рассчитываем лайфтайм для каждой покупки
result_raw['lifetime'] = (
result_raw['event_dt'] - result_raw['first_ts']
).dt.days
# группируем по cohort, если в dimensions ничего нет
if len(dimensions) == 0:
result_raw['cohort'] = 'All users'
dimensions = dimensions + ['cohort']
# функция для группировки таблицы по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
result = df.pivot_table(
index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
)
result = result.fillna(0).cumsum(axis = 1)
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
# делим каждую «ячейку» в строке на размер когорты
# и получаем conversion rate
result = result.div(result['cohort_size'], axis=0)
result = result[['cohort_size'] + list(range(horizon_days))]
result['cohort_size'] = cohort_sizes
return result
# получаем таблицу конверсии
result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)
# для таблицы динамики конверсии убираем 'cohort' из dimensions
if 'cohort' in dimensions:
dimensions = []
# получаем таблицу динамики конверсии
result_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
# возвращаем обе таблицы и сырые данные
return result_raw, result_grouped, result_in_time
# функция для расчёта LTV и ROI
def get_ltv(
profiles,
purchases,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# добавляем данные о покупках в профили
result_raw = result_raw.merge(
purchases[['user_id', 'event_dt', 'revenue']], on='user_id', how='left'
)
# рассчитываем лайфтайм пользователя для каждой покупки
result_raw['lifetime'] = (
result_raw['event_dt'] - result_raw['first_ts']
).dt.days
# группируем по cohort, если в dimensions ничего нет
if len(dimensions) == 0:
result_raw['cohort'] = 'All users'
dimensions = dimensions + ['cohort']
# функция группировки по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
# строим «треугольную» таблицу выручки
result = df.pivot_table(
index=dims, columns='lifetime', values='revenue', aggfunc='sum'
)
# находим сумму выручки с накоплением
result = result.fillna(0).cumsum(axis=1)
# вычисляем размеры когорт
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
# объединяем размеры когорт и таблицу выручки
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
# считаем LTV: делим каждую «ячейку» в строке на размер когорты
result = result.div(result['cohort_size'], axis=0)
# исключаем все лайфтаймы, превышающие горизонт анализа
result = result[['cohort_size'] + list(range(horizon_days))]
# восстанавливаем размеры когорт
result['cohort_size'] = cohort_sizes
# собираем датафрейм с данными пользователей и значениями CAC,
# добавляя параметры из dimensions
cac = df[['user_id', 'acquisition_cost'] + dims].drop_duplicates()
# считаем средний CAC по параметрам из dimensions
cac = (
cac.groupby(dims)
.agg({'acquisition_cost': 'mean'})
.rename(columns={'acquisition_cost': 'cac'})
)
# считаем ROI: делим LTV на CAC
roi = result.div(cac['cac'], axis=0)
# удаляем строки с бесконечным ROI
roi = roi[~roi['cohort_size'].isin([np.inf])]
# восстанавливаем размеры когорт в таблице ROI
roi['cohort_size'] = cohort_sizes
# добавляем CAC в таблицу ROI
roi['cac'] = cac['cac']
# в финальной таблице оставляем размеры когорт, CAC
# и ROI в лайфтаймы, не превышающие горизонт анализа
roi = roi[['cohort_size', 'cac'] + list(range(horizon_days))]
# возвращаем таблицы LTV и ROI
return result, roi
# получаем таблицы LTV и ROI
result_grouped, roi_grouped = group_by_dimensions(
result_raw, dimensions, horizon_days
)
# для таблиц динамики убираем 'cohort' из dimensions
if 'cohort' in dimensions:
dimensions = []
# получаем таблицы динамики LTV и ROI
result_in_time, roi_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
return (
result_raw, # сырые данные
result_grouped, # таблица LTV
result_in_time, # таблица динамики LTV
roi_grouped, # таблица ROI
roi_in_time, # таблица динамики ROI
)
# функция для сглаживания фрейма
def filter_data(df, window):
# для каждого столбца применяем скользящее среднее
for column in df.columns.values:
df[column] = df[column].rolling(window).mean()
return df
# функция для визуализации удержания
def plot_retention(retention, retention_history, horizon, window=7):
# задаём размер сетки для графиков
plt.figure(figsize=(15, 10))
# исключаем размеры когорт и удержание первого дня
retention = retention.drop(columns=['cohort_size', 0])
# в таблице динамики оставляем только нужный лайфтайм
retention_history = retention_history.drop(columns=['cohort_size'])[
[horizon - 1]
]
# если в индексах таблицы удержания только payer,
# добавляем второй признак — cohort
if retention.index.nlevels == 1:
retention['cohort'] = 'All users'
retention = retention.reset_index().set_index(['cohort', 'payer'])
# в таблице графиков — два столбца и две строки, четыре ячейки
# в первой строим кривые удержания платящих пользователей
ax1 = plt.subplot(2, 2, 1)
retention.query('payer == True').droplevel('payer').T.plot(
grid=True, ax=ax1
)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Удержание платящих пользователей')
# во второй ячейке строим кривые удержания неплатящих
# вертикальная ось — от графика из первой ячейки
ax2 = plt.subplot(2, 2, 2, sharey=ax1)
retention.query('payer == False').droplevel('payer').T.plot(
grid=True, ax=ax2
)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Удержание неплатящих пользователей')
# в третьей ячейке — динамика удержания платящих
ax3 = plt.subplot(2, 2, 3)
# получаем названия столбцов для сводной таблицы
columns = [
name
for name in retention_history.index.names
if name not in ['dt', 'payer']
]
# фильтруем данные и строим график
filtered_data = retention_history.query('payer == True').pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax3)
plt.xlabel('Дата привлечения')
plt.title(
'Динамика удержания платящих пользователей на {}-й день'.format(
horizon
)
)
# в чётвертой ячейке — динамика удержания неплатящих
ax4 = plt.subplot(2, 2, 4, sharey=ax3)
# фильтруем данные и строим график
filtered_data = retention_history.query('payer == False').pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax4)
plt.xlabel('Дата привлечения')
plt.title(
'Динамика удержания неплатящих пользователей на {}-й день'.format(
horizon
)
)
plt.tight_layout()
plt.show()
# функция для визуализации конверсии
def plot_conversion(conversion, conversion_history, horizon, window=7):
# задаём размер сетки для графиков
plt.figure(figsize=(15, 5))
# исключаем размеры когорт
conversion = conversion.drop(columns=['cohort_size'])
# в таблице динамики оставляем только нужный лайфтайм
conversion_history = conversion_history.drop(columns=['cohort_size'])[
[horizon - 1]
]
# первый график — кривые конверсии
ax1 = plt.subplot(1, 2, 1)
conversion.T.plot(grid=True, ax=ax1)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Конверсия пользователей')
# второй график — динамика конверсии
ax2 = plt.subplot(1, 2, 2, sharey=ax1)
columns = [
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
name for name in conversion_history.index.names if name not in ['dt']
]
filtered_data = conversion_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax2)
plt.xlabel('Дата привлечения')
plt.title('Динамика конверсии пользователей на {}-й день'.format(horizon))
plt.tight_layout()
plt.show()
# функция для визуализации LTV и ROI
def plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon, window=7):
# задаём сетку отрисовки графиков
plt.figure(figsize=(20, 10))
# из таблицы ltv исключаем размеры когорт
ltv = ltv.drop(columns=['cohort_size'])
# в таблице динамики ltv оставляем только нужный лайфтайм
ltv_history = ltv_history.drop(columns=['cohort_size'])[[horizon - 1]]
# стоимость привлечения запишем в отдельный фрейм
cac_history = roi_history[['cac']]
# из таблицы roi исключаем размеры когорт и cac
roi = roi.drop(columns=['cohort_size', 'cac'])
# в таблице динамики roi оставляем только нужный лайфтайм
roi_history = roi_history.drop(columns=['cohort_size', 'cac'])[
[horizon - 1]
]
# первый график — кривые ltv
ax1 = plt.subplot(2, 3, 1)
ltv.T.plot(grid=True, ax=ax1)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('LTV')
# второй график — динамика ltv
ax2 = plt.subplot(2, 3, 2, sharey=ax1)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in ltv_history.index.names if name not in ['dt']]
filtered_data = ltv_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax2)
plt.xlabel('Дата привлечения')
plt.title('Динамика LTV пользователей на {}-й день'.format(horizon))
# третий график — динамика cac
ax3 = plt.subplot(2, 3, 3, sharey=ax1)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in cac_history.index.names if name not in ['dt']]
filtered_data = cac_history.pivot_table(
index='dt', columns=columns, values='cac', aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax3)
plt.xlabel('Дата привлечения')
plt.title('Динамика стоимости привлечения пользователей')
# четвёртый график — кривые roi
ax4 = plt.subplot(2, 3, 4)
roi.T.plot(grid=True, ax=ax4)
plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('ROI')
# пятый график — динамика roi
ax5 = plt.subplot(2, 3, 5, sharey=ax4)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in roi_history.index.names if name not in ['dt']]
filtered_data = roi_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax5)
plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
plt.xlabel('Дата привлечения')
plt.title('Динамика ROI пользователей на {}-й день'.format(horizon))
plt.tight_layout()
plt.show()
Напишем функцию для анализа профилей пользователей по выбранным критериям.
def users_analytic(df, column):
'''Анализирует пользователей по выбранному признаку. С учетом платящий/неплатящий.
Определяет долю платящих пользователей внутри групп и конверсию в платящего.
'''
# Группировка данных по выбранному признаку, подсчет пользователей в каждой группе и их доли.
users_sum = df.groupby([column]).agg({'user_id' : 'count'}).reset_index()
users_sum['%_total'] = users_sum['user_id'] / users_sum['user_id'].sum() * 100
# Группировка данных по выбранному признаку, подсчет платящих пользователей в каждой группе и их доли
users_payer_analytic = df.query('payer == True').groupby(column).agg({'user_id' : 'count'})\
.reset_index().rename(columns={'user_id':'payers'})
users_payer_analytic['%_payers'] = users_payer_analytic['payers'] / users_payer_analytic['payers'].sum() * 100
# объединение данных по всем пользователям и платящим, сортировка по количеству платящих клиентов
users_payer_analytic = users_sum.merge(users_payer_analytic, on=column)\
.rename(columns={'user_id':'users_total'})\
.sort_values(by='payers', ascending=False)
# рассчет конверсиии в платящего
users_payer_analytic['conversion'] = users_payer_analytic['payers'] / users_payer_analytic['users_total'] * 100
display('Структура платящих пользователей', users_payer_analytic)
# рассчет рекламного бюджета
market_budget = df.groupby(column).agg({'acquisition_cost':'sum'})\
.reset_index().rename(columns={'acquisition_cost':'market_budget'})\
.sort_values(by='market_budget', ascending=False)
market_budget['%'] = market_budget['market_budget'] / market_budget['market_budget'].sum() * 100
display('Рекламный бюджет', market_budget)
Получим профили пользователей.
profiles = get_profiles(visits, orders, costs)
print(profiles.head(5))
user_id first_ts channel device region dt \
0 599326 2019-05-07 20:58:57 FaceBoom Mac United States 2019-05-07
1 4919697 2019-07-09 12:46:07 FaceBoom iPhone United States 2019-07-09
2 6085896 2019-10-01 09:58:33 organic iPhone France 2019-10-01
3 22593348 2019-08-22 21:35:48 AdNonSense PC Germany 2019-08-22
4 31989216 2019-10-02 00:07:44 YRabbit iPhone United States 2019-10-02
month payer acquisition_cost
0 2019-05-01 True 1.09
1 2019-07-01 False 1.11
2 2019-10-01 False 0.00
3 2019-08-01 False 0.99
4 2019-10-01 False 0.23
Определим минимальную и максимальную даты привлечения пользователей.
min_user_date = profiles['dt'].min()
max_user_date = profiles['dt'].max()
print(min_user_date)
print(max_user_date)
2019-05-01 00:00:00 2019-10-27 00:00:00
users_analytic(profiles, 'region')
'Структура платящих пользователей'
| region | users_total | %_total | payers | %_payers | conversion | |
|---|---|---|---|---|---|---|
| 3 | United States | 100002 | 66.66 | 6902 | 77.72 | 6.90 |
| 2 | UK | 17575 | 11.72 | 700 | 7.88 | 3.98 |
| 0 | France | 17450 | 11.63 | 663 | 7.47 | 3.80 |
| 1 | Germany | 14981 | 9.99 | 616 | 6.94 | 4.11 |
'Рекламный бюджет'
| region | market_budget | % | |
|---|---|---|---|
| 3 | United States | 90928.60 | 86.19 |
| 0 | France | 5075.97 | 4.81 |
| 2 | UK | 5068.50 | 4.80 |
| 1 | Germany | 4424.22 | 4.19 |
users_analytic(profiles, 'device')
'Структура платящих пользователей'
| device | users_total | %_total | payers | %_payers | conversion | |
|---|---|---|---|---|---|---|
| 3 | iPhone | 54479 | 36.32 | 3382 | 38.08 | 6.21 |
| 0 | Android | 35032 | 23.35 | 2050 | 23.08 | 5.85 |
| 1 | Mac | 30042 | 20.03 | 1912 | 21.53 | 6.36 |
| 2 | PC | 30455 | 20.30 | 1537 | 17.31 | 5.05 |
'Рекламный бюджет'
| device | market_budget | % | |
|---|---|---|---|
| 3 | iPhone | 43678.79 | 41.40 |
| 1 | Mac | 24094.54 | 22.84 |
| 0 | Android | 22460.63 | 21.29 |
| 2 | PC | 15263.34 | 14.47 |
users_analytic(profiles, 'channel')
'Структура платящих пользователей'
| channel | users_total | %_total | payers | %_payers | conversion | |
|---|---|---|---|---|---|---|
| 1 | FaceBoom | 29144 | 19.43 | 3557 | 40.05 | 12.20 |
| 6 | TipTop | 19561 | 13.04 | 1878 | 21.15 | 9.60 |
| 10 | organic | 56439 | 37.62 | 1160 | 13.06 | 2.06 |
| 7 | WahooNetBanner | 8553 | 5.70 | 453 | 5.10 | 5.30 |
| 0 | AdNonSense | 3880 | 2.59 | 440 | 4.95 | 11.34 |
| 5 | RocketSuperAds | 4448 | 2.97 | 352 | 3.96 | 7.91 |
| 2 | LeapBob | 8553 | 5.70 | 262 | 2.95 | 3.06 |
| 4 | OppleCreativeMedia | 8605 | 5.74 | 233 | 2.62 | 2.71 |
| 9 | lambdaMediaAds | 2149 | 1.43 | 225 | 2.53 | 10.47 |
| 8 | YRabbit | 4312 | 2.87 | 165 | 1.86 | 3.83 |
| 3 | MediaTornado | 4364 | 2.91 | 156 | 1.76 | 3.57 |
'Рекламный бюджет'
| channel | market_budget | % | |
|---|---|---|---|
| 6 | TipTop | 54751.30 | 51.90 |
| 1 | FaceBoom | 32445.60 | 30.75 |
| 7 | WahooNetBanner | 5151.00 | 4.88 |
| 0 | AdNonSense | 3911.25 | 3.71 |
| 4 | OppleCreativeMedia | 2151.25 | 2.04 |
| 5 | RocketSuperAds | 1833.00 | 1.74 |
| 2 | LeapBob | 1797.60 | 1.70 |
| 9 | lambdaMediaAds | 1557.60 | 1.48 |
| 3 | MediaTornado | 954.48 | 0.90 |
| 8 | YRabbit | 944.22 | 0.90 |
| 10 | organic | 0.00 | 0.00 |
66% пользователей продукта проживают в США, по 12% в Англии и Франции, 10% в Германии. Среди платящих пользоваателей на США приходится 78%, доля остальных стран около 7-8%. 86% рекламного бюджета расходуется на привлечение пользователей из США.
60% клиентов пользуются продуктом через мобильное приложение (36% с Phone, 24% с Android), 40% через десктопную версию в равной мере PC и Mac. Лучше всего конвертируются в платящих пользователи Maс - 6.36% стоит обратить на них внимание, так как их доля среди клиентов минимальна 20.03%
37.62% пользователей органические, их конверсия в платящих минимальна 2.06% (13.06 платящих пользователей). 19.43% приходят из FaceBoom и стоят нам 31% рекламного бюджета. Максимальная для каналов конверсия в 12.2% дает 40.05% платящих пользователей. 13.04% приходят из TipTop и стоят нам 52% рекламного бюджета. Данный канал приводит 21.15% платящих пользователей. Доля остальных каналов привлечения составляет 1,43-5.7%. Из них высокая конверсия у AdNonSense - 11.34% и lambdaMediaAds - 10.47%.
costs.head()
| dt | channel | costs | |
|---|---|---|---|
| 0 | 2019-05-01 | FaceBoom | 113.30 |
| 1 | 2019-05-02 | FaceBoom | 78.10 |
| 2 | 2019-05-03 | FaceBoom | 85.80 |
| 3 | 2019-05-04 | FaceBoom | 136.40 |
| 4 | 2019-05-05 | FaceBoom | 122.10 |
Посчитаем общую сумму расходов на маркетинг.
costs['costs'].sum()
105497.30000000002
Посчитаем расходы на маркетинг в разрезе каналов привлечения.
costs.groupby('channel').agg({'costs':'sum'}).sort_values(by='costs').reset_index()
| channel | costs | |
|---|---|---|
| 0 | YRabbit | 944.22 |
| 1 | MediaTornado | 954.48 |
| 2 | lambdaMediaAds | 1557.60 |
| 3 | LeapBob | 1797.60 |
| 4 | RocketSuperAds | 1833.00 |
| 5 | OppleCreativeMedia | 2151.25 |
| 6 | AdNonSense | 3911.25 |
| 7 | WahooNetBanner | 5151.00 |
| 8 | FaceBoom | 32445.60 |
| 9 | TipTop | 54751.30 |
Построим визуализацию динамики изменения расходов во времени (по неделям и месяцам) по каждому источнику.
filter_data(costs.pivot_table(index='dt', columns='channel', values='costs', aggfunc='sum'), 4).plot(
grid=True, figsize=(20, 15))
plt.xlabel('Период')
plt.title('Динамика расходов на маркетинг', fontsize = 15)
Text(0.5, 1.0, 'Динамика расходов на маркетинг')
Исключим два самых дорогих канала привлечения.
filter_data(costs.pivot_table(index='dt', columns='channel', values='costs', aggfunc='sum')\
.drop(columns=['TipTop', 'FaceBoom']), 7).plot(
grid=True, figsize=(20, 15))
plt.xlabel('Период')
plt.title('Динамика расходов на маркетинг')
Text(0.5, 1.0, 'Динамика расходов на маркетинг')
Узнаем, сколько в среднем стоило привлечение одного пользователя (CAC) из каждого источника.
cac = profiles.groupby('channel')\
.agg({'acquisition_cost':'sum', 'user_id': 'nunique', 'payer':'sum'}).reset_index()
cac['cac_user'] = cac['acquisition_cost'] / cac['user_id']
cac['cac_payer'] = cac['acquisition_cost'] / cac['payer']
cac.sort_values(by='cac_payer')
| channel | acquisition_cost | user_id | payer | cac_user | cac_payer | |
|---|---|---|---|---|---|---|
| 10 | organic | 0.00 | 56439 | 1160 | 0.00 | 0.00 |
| 5 | RocketSuperAds | 1833.00 | 4448 | 352 | 0.41 | 5.21 |
| 8 | YRabbit | 944.22 | 4312 | 165 | 0.22 | 5.72 |
| 3 | MediaTornado | 954.48 | 4364 | 156 | 0.22 | 6.12 |
| 2 | LeapBob | 1797.60 | 8553 | 262 | 0.21 | 6.86 |
| 9 | lambdaMediaAds | 1557.60 | 2149 | 225 | 0.72 | 6.92 |
| 0 | AdNonSense | 3911.25 | 3880 | 440 | 1.01 | 8.89 |
| 1 | FaceBoom | 32445.60 | 29144 | 3557 | 1.11 | 9.12 |
| 4 | OppleCreativeMedia | 2151.25 | 8605 | 233 | 0.25 | 9.23 |
| 7 | WahooNetBanner | 5151.00 | 8553 | 453 | 0.60 | 11.37 |
| 6 | TipTop | 54751.30 | 19561 | 1878 | 2.80 | 29.15 |
Проанализируем окупаемость рекламы c помощью графиков LTV и ROI, а также графиков динамики LTV, CAC и ROI. Исключим из анализа органических пользователей, так как расходы на рекламу влияют на них косвенно.
horizon_days = 14
observation_date = datetime(2019, 11, 1).date()
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles.query('channel != "organic"'),
orders,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
)
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days)
Посмотрим на удержание и конверсию пользователей.
# смотрим конверсию
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles.query('channel != "organic"'),
orders, observation_date, horizon_days, dimensions=[]
)
plot_conversion(conversion_grouped, conversion_history, horizon_days)
Привлекаемые пользователи стабильно конвертируются в платящих.
# смотрим удержание
retention_raw, retention_grouped, retention_history = get_retention(
profiles.query('channel != "organic"'),
visits, observation_date, horizon_days, dimensions=[]
)
plot_retention(retention_grouped, retention_history, horizon_days)
# смотрим окупаемость с разбивкой по странам
dimensions = ['region']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles.query('channel != "organic"'), orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
# посчитаем месячный рекламный бюджет в разрезе стран
profiles.pivot_table(index='region', columns='month', values='acquisition_cost', aggfunc='sum')
| month | 2019-05-01 | 2019-06-01 | 2019-07-01 | 2019-08-01 | 2019-09-01 | 2019-10-01 |
|---|---|---|---|---|---|---|
| region | ||||||
| France | 801.03 | 850.72 | 793.62 | 953.21 | 877.13 | 800.26 |
| Germany | 727.20 | 679.52 | 675.33 | 797.81 | 801.25 | 743.10 |
| UK | 828.69 | 813.19 | 785.68 | 898.04 | 899.65 | 843.25 |
| United States | 7621.64 | 12746.61 | 14192.43 | 18008.81 | 19863.31 | 18495.80 |
# смотрим конверсию с разбивкой по странам
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles.query('channel != "organic"'), orders, observation_date, horizon_days, dimensions=dimensions
)
plot_conversion(conversion_grouped, conversion_history, horizon_days)
Пользователи из США стабильно конвертируются в платящих, причем почти в 2 раза лучше пользователей из других стран.
# смотрим удержание с разбивкой по странам
retention_raw, retention_grouped, retention_history = get_retention(
profiles.query('channel != "organic"'), visits, observation_date, horizon_days, dimensions=dimensions
)
plot_retention(retention_grouped, retention_history, horizon_days)
Удержание пользователей из США самое слабое, причем оно стабильно низкое для всего периода. Стоит работать в направлении удержания пользователей из самой крупной группы.
# смотрим окупаемость с разбивкой по устройствам
dimensions = ['device']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles.query('channel != "organic"'), orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
# смотрим конверсию с разбивкой по устройствам
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles.query('channel != "organic"'), orders, observation_date, horizon_days, dimensions=dimensions
)
plot_conversion(conversion_grouped, conversion_history, horizon_days)
Лучше всего конвертируются пользователи Mac и iPhone. У пользователей РС самая низкая и нестабильная конвертация.
# смотрим удержание с разбивкой по устройствам
retention_raw, retention_grouped, retention_history = get_retention(
profiles.query('channel != "organic"'), visits, observation_date, horizon_days, dimensions=dimensions
)
plot_retention(retention_grouped, retention_history, horizon_days)
Удержание для пользователей Mac и iPhone ниже, чем для пользователей РС и Android.
# смотрим окупаемость с разбивкой по каналам
dimensions = ['channel']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles.query('channel != "organic"'), orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=20
)
# посчитаем месячный рекламный бюджет в разрезе каналов
profiles.pivot_table(index='channel', columns='month', values='acquisition_cost', aggfunc='sum')
| month | 2019-05-01 | 2019-06-01 | 2019-07-01 | 2019-08-01 | 2019-09-01 | 2019-10-01 |
|---|---|---|---|---|---|---|
| channel | ||||||
| AdNonSense | 1169.70 | 538.65 | 504.00 | 579.60 | 581.70 | 537.60 |
| FaceBoom | 3524.40 | 5501.10 | 5294.30 | 6274.40 | 6114.90 | 5736.50 |
| LeapBob | 140.28 | 314.58 | 313.53 | 369.81 | 343.98 | 315.42 |
| MediaTornado | 238.56 | 138.00 | 138.48 | 154.56 | 144.72 | 140.16 |
| OppleCreativeMedia | 169.75 | 370.00 | 366.50 | 439.25 | 427.75 | 378.00 |
| RocketSuperAds | 577.98 | 325.72 | 252.07 | 253.11 | 218.40 | 205.72 |
| TipTop | 2981.00 | 6675.60 | 8410.20 | 11202.00 | 13232.50 | 12250.00 |
| WahooNetBanner | 418.80 | 921.00 | 851.40 | 1040.40 | 977.40 | 942.00 |
| YRabbit | 299.70 | 106.20 | 97.38 | 124.74 | 152.79 | 163.41 |
| lambdaMediaAds | 458.40 | 199.20 | 219.20 | 220.00 | 247.20 | 213.60 |
| organic | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 |
# смотрим конверсию с разбивкой по каналам
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles.query('channel != "organic"'), orders, observation_date, horizon_days, dimensions=dimensions
)
plot_conversion(conversion_grouped, conversion_history, horizon_days)
Пользователи со всех каналов конвертируются. Самая высокая конверсия у пользователей с неокупаемых каналов TipTop, FaceBoom и AdNonSense, за исключением lambdaMediaAds.
# смотрим удержание с разбивкой по устройствам
retention_raw, retention_grouped, retention_history = get_retention(
profiles.query('channel != "organic"'), visits, observation_date, horizon_days, dimensions=dimensions
)
plot_retention(retention_grouped, retention_history, horizon_days)
Пользователи, привлеченные на FaceBoom и AdNonSense очень плохо удерживаются.
Регион:
Стоит работать в направлении удержания пользователей из самой крупной группы.
Окупаемость рекламных вложений не зависит от типа устройства.
Стоит обратить внимание на продукт и рекламу для клиентов Mac и iPhone. У них высокая конверсия, но низкое удержание. Возможно у нас проблемы с системой iOS или мы не удовлетворяем специфических ожиданий пользователей.
Рекламные каналы разделены по региональному признаку.
80% рекламного бюджета тратится на привлечение неокупаемых пользователей через TipTop, FaceBoom. Стоит оценить целесообразность дальнейшего сотрудничества с данными каналами. Пользователей с FaceBoom мы не можем удержать, а цены TipTop непомерно высоки. RocketSuperAds выглядит перспективным для расширения использования.
Стоит дополнительно изучить аудиторию AdNonSence: почему она хорошо конвертируется и плохо удерживается. Возможно это клиенты, чье ожидание не соответствуют нашему продукту? Или в рекламе на этом канале мы даем неправильное понятие о содержании нашего продукта? Или это неплатежеспособная аудитория?
Мы исследовали данные о пользовательской активности в приложении Procrastinate Pro+ и расходах на их привлечение за период с 1 мая по 27 октября 2019 года.
Обзор и предобработка показали, что данные "чистые". Мы преобразовали колонки с датами в формат datetime и привели наименование колонок в соответствие со стилем.
Пользователи из Франции, Германии и Англии окупаются на 4-6 день. Пользователи из США не окупаются. САС пользователей из США в 3-4 раза превышает CAC остальных стран 0.8-1.8 против 0.4-0.6. LTV пользователей достаточно стабилен, при этом пользователи из США незначительно выделяются на графиках (ожидаем, что 66% пользователей будут приносить значительно больше дохода, чем группы по 10-12%). Пользователи из США стабильно конвертируются в платящих: почти 10% к 14 дню, что в 2 раза лучше пользователей из других стран (5%). Удержание пользователей из США самое слабое, оно стабильно низкое для всего периода 0.35-0.1.
Окупаемость рекламных вложений не зависит от типа устройства: в США не окупаются пользователи всех устройств, в Европейском регионе - окупаются. Вцелом окупились только пользователи на РС - самая малочисленная группа наших пользователей 17.3% и самая дешевая по стоимости привлечения, при этом у них самая низкая и нестабильная конверсия.
Всреднем привлечение 1 пользователя обошлось нам в 0.21-2.8 доллара. Самый дорогой канал TipTop - 2.8. Наиболее дешево обошлись пользователи с LeapBob, MediaTornado и YRabbit. Стоимость привлечения платящих пользователей варьируется от 5.21 на RocketSuperAds до 29.15 на TipTop.
Рекламные каналы разделены по региональному признаку.
В странах Европейского региона мы используем AdNonSense, LeapBob, OppleCreativeMedia, WahooNetBanner и lambdaMediaAds.
Рекомендации:
Я провела дополнительное исследование. Посмотрим что происходит с окупаемостью рекламы в каждом конкретном регионе. Для этого я объединила в новую функцию LTV, ROI, CAC, конверсию и удержание. Исключила органических пользователей (для всех стран кроме Англии, в Англии до 14 дня доживают только органические пользователи).
Данное дополнительное исследование позволила найти некоторые особенности (например, что каналы привлечения четко поделены на США и Европейский рынок, что внутри Европейского рынка поведение клиентов на одних и тех же каналах неодинаково - этого не видно из анализа в п.6.1.3 так как внутри европейских каналов привлечения сидят клиенты из трех стран).
def analytic_by_contries(country, dimensions):
'''Анализируем окупаемость рекламы c помощью графиков LTV и ROI,
а также графиков динамики LTV, CAC и ROI.
Исключаем из анализа органических пользователей.
Строим графики конверсии и удержания.
'''
# смотрим окупаемость с разбивкой по выбранному критерию
dimensions = [dimensions]
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles.query('(channel != "organic") & (region == @country)'),
orders, observation_date, horizon_days, dimensions=dimensions)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=20)
# смотрим конверсию
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles.query('(channel != "organic") & (region == @country)'),
orders, observation_date, horizon_days, dimensions=dimensions)
plot_conversion(conversion_grouped, conversion_history, horizon_days)
# смотрим удержание
retention_raw, retention_grouped, retention_history = get_retention(
profiles.query('(channel != "organic") & (region == @country)'),
visits, observation_date, horizon_days, dimensions= dimensions)
plot_retention(retention_grouped, retention_history, horizon_days)
def analytic_by_contries1(country, dimensions):
'''Анализируем окупаемость рекламы c помощью графиков LTV и ROI,
а также графиков динамики LTV, CAC и ROI.
Исключаем из анализа органических пользователей.
Строим графики конверсии и удержания.
'''
# смотрим окупаемость с разбивкой по выбранному критерию
dimensions = [dimensions]
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
profiles.query('(region == @country)'),
orders, observation_date, horizon_days, dimensions=dimensions)
plot_ltv_roi(
ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=20)
# смотрим конверсию
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles.query('(region == @country)'),
orders, observation_date, horizon_days, dimensions=dimensions)
plot_conversion(conversion_grouped, conversion_history, horizon_days)
# смотрим удержание
retention_raw, retention_grouped, retention_history = get_retention(
profiles.query('(region == @country)'),
visits, observation_date, horizon_days, dimensions= dimensions)
plot_retention(retention_grouped, retention_history, horizon_days)
Рассмотрим более подробно пользователей из США. Так как только они не окупаются, насколько каналы привлечения и устройства влияют на это.
analytic_by_contries('United States', 'channel')
Пользователи с FaceBoom очень хорошо конвертируются и очень плохо удерживаются. Пользователи с TipTop хорошо конвертируются, удержания их не хуже друих каналов, но не окупаются.
analytic_by_contries('United States', 'device')
Тип устройства не влияет на окупаемость пользователей из США.
Пользователи Android в США конвертируются чуть лучше остальных. Удержание не зависит от устройства.
Рассмотрим более подробно пользователей из Англии, они окупаются лучше остальных. Насколько каналы привлечения и устройства влияют на это. Можно ли применить этот опыт на пользователях из других стран.
analytic_by_contries1('UK', 'channel')
# посчитаем месячный рекламный бюджет в разрезе каналов
market_budget = profiles.query('(channel != "organic") & (region == "UK")')\
.pivot_table(index='channel',
columns='month',
values='acquisition_cost',
aggfunc='sum')
market_budget['sum'] = market_budget.sum(axis=1)
market_budget['sum_%'] = market_budget['sum'] / market_budget['sum'].sum() * 100
market_budget
| month | 2019-05-01 00:00:00 | 2019-06-01 00:00:00 | 2019-07-01 00:00:00 | 2019-08-01 00:00:00 | 2019-09-01 00:00:00 | 2019-10-01 00:00:00 | sum | sum_% |
|---|---|---|---|---|---|---|---|---|
| channel | ||||||||
| AdNonSense | 402.58 | 179.65 | 172.80 | 177.29 | 193.68 | 179.64 | 1305.65 | 25.76 |
| LeapBob | 51.04 | 108.82 | 118.91 | 130.27 | 124.54 | 106.03 | 639.61 | 12.62 |
| OppleCreativeMedia | 63.02 | 128.42 | 128.72 | 154.08 | 153.45 | 135.49 | 763.18 | 15.06 |
| WahooNetBanner | 147.67 | 321.11 | 290.46 | 361.45 | 338.68 | 349.52 | 1808.89 | 35.69 |
| lambdaMediaAds | 164.38 | 75.20 | 74.80 | 74.95 | 89.28 | 72.57 | 551.17 | 10.87 |
Пользователи AdNonSens конвертируются лучше всех. Но их удержание в 2 раза хуже, чем с остальных каналов. Возможно это не наши пользователи?
analytic_by_contries1('UK', 'device')
В Англии лучше всего конвертируются пользователи РС, хуже всего - пользователи Mac.
analytic_by_contries('France', 'channel')
Пользователи, привлеченные в AdNonSence, не окупаются и для Франции. При этом они хорошо конвертируются и плохо удерживаются.
analytic_by_contries('France', 'device')
Во Франции лучше всего окупаются пользователи Мас, хуже всего - Android.
analytic_by_contries('Germany', 'channel')
Пользователи, привлеченные в AdNonSence, не окупаются и в Германии. При этом они хорошо конвертируются и плохо удерживаются.
analytic_by_contries('Germany', 'device')
В Германии лучше всего конвертируются пользователи PC и Android. Окупаются пользователи всех устройств, но хуже всего пользователи Мас.
Окупаемость рекламных вложений не зависит от типа устройства.
В Англии ROI пользователей Mac, привлеченных в июле, и пользователей iPhone, привлеченных в сентябре, ниже 1. Возможно у нас нерегулярные проблемы с системой iOS.
Рекламные каналы разделены по региональному признаку.
В США мы привлекаем пользователей через TipTop, FaceBoom, YRabbit, MediaTornado и RocketSuperAds.
В странах Европейского региона мы используем AdNonSense, LeapBob, OppleCreativeMedia, WahooNetBanner и lambdaMediaAds.
Канал WahooNetBanner требует внимания: только на территории Англии ROI снижается.